package csrb;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.tidy.Tidy;
import org.xml.sax.SAXException;

import com.google.appengine.api.images.Composite;
import com.google.appengine.api.images.Image;
import com.google.appengine.api.images.ImagesService;
import com.google.appengine.api.images.ImagesServiceFactory;
import com.google.appengine.api.images.Transform;
import com.google.appengine.api.memcache.Expiration;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.memcache.Stats;

@SuppressWarnings("serial")
public class BannerGeneratorServlet extends HttpServlet {

	private static final int LETTER_WIDTH = 8;

	private static final String CACHE_KEY_AVAILABILITY = "_csr_available";

	private static final String CACHE_KEY_CSR_ERROR = "_csr_error_cnt";

	private static final String CACHE_KEY_BLANK = "_blank_img";

	private static final long CACHE_EXPIRE = 60 * 60 * 1000; // 60 minutes

	private MemcacheService cacheService;

	private ImagesService imagesService;

	private Tidy tidy;

	private Map<Character, Image> letters;

	private Map<String, Image> backgrounds;

	private String CHECK_PID = "696839";

	private static final int CHECK_THRESHOLD = 3;

	private String CSR_URL = "http://csr.wwiionline.com/scripts/services/persona/service.jsp?pid=";

	private Logger log = Logger.getLogger(getClass().getName());

	/**
	 * @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);

		if (log.isLoggable(Level.INFO)) {
			log.info("Initializing banner generator servlet.");
		}

		String url = config.getInitParameter("CSR_URL");
		if (url != null) {
			this.CSR_URL = url.trim();
		}

		String pid = config.getInitParameter("CHECK_PID");
		if (pid != null) {
			this.CHECK_PID = pid.trim();
		}

		this.tidy = new Tidy();

		tidy.setQuiet(true);
		tidy.setShowWarnings(false);
		tidy.setXmlOut(true);

		try {
			this.imagesService = ImagesServiceFactory.getImagesService();

			this.cacheService = MemcacheServiceFactory.getMemcacheService();

			this.backgrounds = (Map<String, Image>) cacheService
					.get("backgrounds");

			if (backgrounds == null) {
				backgrounds = new HashMap<String, Image>(6);

				if (log.isLoggable(Level.FINE)) {
					log.fine("Reading background images...");
				}

				for (Country country : Country.values()) {
					for (Branch branch : Branch.values()) {
						createBackground(country, branch, backgrounds);
					}
				}

				createSquadBackground(backgrounds);

				cacheService.put("backgrounds", backgrounds);
			} else {
				if (log.isLoggable(Level.FINE)) {
					log.fine("Using cached background images...");
				}
			}

			this.letters = (Map<Character, Image>) cacheService.get("letters");

			if (letters == null) {
				letters = new HashMap<Character, Image>(64);

				InputStream in = getClass().getResourceAsStream(
						"resource/letters.png");
				byte[] data = IOUtils.toByteArray(in);
				IOUtils.closeQuietly(in);

				String text = "1234567890.,/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

				if (log.isLoggable(Level.INFO)) {
					log.info("Reading letter images...");
				}

				for (int i = 0; i < text.length(); i++) {
					Image template = ImagesServiceFactory.makeImage(data);

					float width = template.getWidth();
					Character c = text.charAt(i);
					float offset = LETTER_WIDTH * i;

					Transform transform = ImagesServiceFactory.makeCrop(offset
							/ width, 0, (offset + LETTER_WIDTH) / width, 1.0f);
					Image image = imagesService.applyTransform(transform,
							template);

					if (log.isLoggable(Level.FINER)) {
						log.finer("Offset X : " + offset);
						log.finer("Image width : " + image.getWidth());
						log.finer("Image height : " + image.getHeight());
					}

					letters.put(c, image);
				}

				cacheService.put("letters", letters);
			} else {
				if (log.isLoggable(Level.FINE)) {
					log.fine("Using cached letter images...");
				}
			}

			InputStream in = getClass().getResourceAsStream(
					"resource/blank.png");

			cacheService.put(CACHE_KEY_BLANK,
					new BannerCache(IOUtils.toByteArray(in)));

			IOUtils.closeQuietly(in);
		} catch (Exception e) {
			throw new ServletException(e);
		}
	}

	/**
	 * @param country
	 * @param branch
	 * @param images
	 * @throws IOException
	 */
	protected void createBackground(Country country, Branch branch,
			Map<String, Image> images) throws IOException {
		String key = country.name() + "-" + branch.name();

		if (log.isLoggable(Level.FINE)) {
			log.fine("Creating background images for " + key);
		}

		String path = String.format("resource/background-%s.png", key);

		InputStream in = getClass().getResourceAsStream(path);

		Image image = ImagesServiceFactory.makeImage(IOUtils.toByteArray(in));
		IOUtils.closeQuietly(in);

		if (log.isLoggable(Level.FINER)) {
			log.finer("Image path : " + path);
			log.finer("Image width : " + image.getWidth());
			log.finer("Image height : " + image.getHeight());
		}

		images.put(key, image);
	}

	protected void createSquadBackground(Map<String, Image> images)
			throws IOException {
		if (log.isLoggable(Level.FINE)) {
			log.fine("Creating background images JG51 Molders");
		}

		String path = "resource/background-JG51.png";
		InputStream in = getClass().getResourceAsStream(path);

		Image image = ImagesServiceFactory.makeImage(IOUtils.toByteArray(in));
		IOUtils.closeQuietly(in);

		if (log.isLoggable(Level.FINER)) {
			log.finer("Image path : " + path);
			log.finer("Image width : " + image.getWidth());
			log.finer("Image height : " + image.getHeight());
		}

		images.put("JG51", image);
	}

	/**
	 * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
	 *      javax.servlet.http.HttpServletResponse)
	 */
	@Override
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws IOException {
		String url = request.getRequestURI();

		if (log.isLoggable(Level.FINE)) {
			log.fine("Handling request URI : " + url);
		}

		if (url.endsWith("check")) {
			checkAvailability(request, response);
			return;
		}

		if (!url.endsWith(".png")) {
			String msg = "URL must end with \".png\" : " + url;
			response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
			return;
		}

		int index = url.lastIndexOf("/");

		String pid = url.substring(index + 1, url.length() - 4);

		BannerCache cache = null;

		String update = request.getParameter("update");
		if (update == null || !"true".equals(update)) {
			cache = (BannerCache) cacheService.get(pid);
		} else {
			if (log.isLoggable(Level.INFO)) {
				log.info("'update=true' parameter specified by profile : "
						+ pid);
			}
		}

		if (cache != null) {
			long modifiedSince = request.getDateHeader("If-Modified-Since");

			if (modifiedSince != -1) {
				long lastModified = cache.getLastModified();

				Calendar lastModifiedCal = Calendar.getInstance();
				Calendar modifiedSinceCal = Calendar.getInstance();

				lastModifiedCal.setTimeInMillis(lastModified);
				lastModifiedCal.set(Calendar.MILLISECOND, 0);

				modifiedSinceCal.setTimeInMillis(modifiedSince);
				modifiedSinceCal.set(Calendar.MILLISECOND, 0);

				if (modifiedSinceCal.compareTo(lastModifiedCal) > -1) {
					if (log.isLoggable(Level.FINE)) {
						log.fine("Using client cache for PID : " + pid);
					}

					response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
					return;
				} else {
					if (log.isLoggable(Level.FINE)) {
						log.fine("Client cache is outdated for PID : " + pid);
					}
				}
			} else {
				if (log.isLoggable(Level.FINE)) {
					log.fine("Using cached image for PID : " + pid);
				}
			}
		}

		if (cache == null) {
			if (isCSRAvailable()) {
				if (log.isLoggable(Level.FINE)) {
					log.fine("No cache found for PID : " + pid);
				}

				try {
					Profile profile = retrieveProfile(pid);

					if (profile == null) {
						if (log.isLoggable(Level.FINE)) {
							log.fine("Failed to retrieve stats for PID : "
									+ pid);
						}
					} else {
						String jg51 = request.getParameter("jg51");
						boolean squad = (jg51 != null && "true".equals(jg51));

						Image image = createBanner(pid, profile, squad);

						cache = new BannerCache(image.getImageData());

						cacheService.put(pid, cache,
								Expiration.byDeltaMillis((int) CACHE_EXPIRE));

						if (log.isLoggable(Level.FINE)) {
							Stats stats = cacheService.getStatistics();
							log.fine("Current cache size : "
									+ stats.getItemCount());
						}
					}
				} catch (Exception e) {
					if (log.isLoggable(Level.SEVERE)) {
						log.log(Level.SEVERE,
								"Failed to generate banner image for PID : "
										+ pid, e);
					}
				}
			}

			if (cache == null) {
				if (log.isLoggable(Level.FINER)) {
					log.finer("Serving blank image : " + pid);
				}

				cache = (BannerCache) cacheService.get(CACHE_KEY_BLANK);
			}
		}

		response.setContentType("image/png");
		response.setContentLength((int) cache.getData().length);
		response.setDateHeader("Last-Modified", cache.getLastModified());

		OutputStream out = response.getOutputStream();

		IOUtils.write(cache.getData(), out);

		out.flush();

		IOUtils.closeQuietly(out);
	}

	/**
	 * @param request
	 * @param response
	 */
	protected void checkAvailability(HttpServletRequest request,
			HttpServletResponse response) throws IOException {
		Profile profile = null;

		Integer count = (Integer) cacheService.get(CACHE_KEY_CSR_ERROR);
		if (count == null) {
			count = 0;
		}

		try {
			profile = retrieveProfile(CHECK_PID);
		} catch (Exception e) {
			if (log.isLoggable(Level.FINEST)) {
				log.finest("Unable to retrieve data from the CS&R website.");
			}
		}

		if (profile == null) {
			cacheService.put(CACHE_KEY_CSR_ERROR, ++count);

			if (isCSRAvailable()) {
				if (log.isLoggable(Level.INFO)) {
					log.info("Unable to retrieve data from the CS&R website. Last attempt #"
							+ count);
				}

				if (count >= CHECK_THRESHOLD) {
					if (log.isLoggable(Level.INFO)) {
						log.info("The CS&R website is down. Disabling the banner service.");
					}

					setCSRAvailable(false);
				}
			} else if (log.isLoggable(Level.FINE)) {
				log.fine("Unable to retrieve data from the CS&R website. Last attempt #"
						+ count);
			}
		} else if (!isCSRAvailable()) {
			if (log.isLoggable(Level.INFO)) {
				log.info("The CS&R website is back online. Enabling the banner service.");
			}

			cacheService.delete(CACHE_KEY_CSR_ERROR);
			setCSRAvailable(true);
		}
	}

	protected boolean isCSRAvailable() {
		Boolean available = (Boolean) cacheService.get(CACHE_KEY_AVAILABILITY);
		return available == null || available;
	}

	protected void setCSRAvailable(boolean available) {
		cacheService.put(CACHE_KEY_AVAILABILITY, available);
	}

	/**
	 * @param pid
	 * @return
	 * @throws IOException
	 * @throws IllegalStateException
	 * @throws SAXException
	 * @throws ParseException
	 */
	protected Profile retrieveProfile(String pid) throws IOException,
			IllegalStateException, SAXException, ParseException {
		if (log.isLoggable(Level.FINE)) {
			log.fine("Retrieving user statistics for PID : " + pid);
		}

		Profile profile = null;

		URL url = new URL(CSR_URL + pid);

		URLConnection connection = url.openConnection();
		connection.setReadTimeout(10000);
		connection.setConnectTimeout(10000);

		InputStream in = (InputStream) connection.getContent();

		if (in != null) {
			OutputStream debugOut = null;

			if (log.isLoggable(Level.FINEST)) {
				debugOut = System.out;
			}

			Document document = tidy.parseDOM(in, debugOut);

			IOUtils.closeQuietly(in);

			Map<String, String> values = new HashMap<String, String>(30);

			NodeList rows = document.getElementsByTagName("tr");

			for (int i = 0; i < rows.getLength(); i++) {
				Node row = rows.item(i);

				NodeList cells = row.getChildNodes();

				if (cells != null && cells.getLength() == 2) {
					Node node = cells.item(0);
					Node child = node.getFirstChild();

					if (child != null && child.getNodeName().equals("b")) {
						String label = child.getFirstChild().getNodeValue()
								.trim();
						label = label.substring(0, label.length() - 1);

						Node valueChild = cells.item(1).getLastChild();

						if (valueChild != null) {
							String value = valueChild.getNodeValue().trim();
							values.put(label, value);

							if (log.isLoggable(Level.FINER)) {
								log.finer(label + "=" + value);
							}
						}
					}
				}
			}

			profile = Profile.create(values);
		}

		return profile;
	}

	protected Image createBanner(String pid, Profile profile, boolean squad) {
		if (log.isLoggable(Level.FINE)) {
			log.fine("Generating banner image for PID " + pid);
		}

		Image background;

		if (squad && profile.getBranch() == Branch.AirForce) {
			background = backgrounds.get("JG51");

			if (log.isLoggable(Level.FINE)) {
				log.fine("Using JG51 background for PID " + pid);
			}
		} else {
			background = backgrounds.get(profile.getCountry().name() + "-"
					+ profile.getBranch().name());
		}

		Collection<Composite> composites = new ArrayList<Composite>();

		Composite backgroundComposite = ImagesServiceFactory.makeComposite(
				background, 0, 0, 1.0f, Composite.Anchor.TOP_LEFT);

		composites.add(backgroundComposite);
		composites
				.addAll(createComposites(profile.getCallsign(), 82, 11, false));

		background = imagesService.composite(composites, background.getWidth(),
				background.getHeight(), 0);
		backgroundComposite = ImagesServiceFactory.makeComposite(background, 0,
				0, 1.0f, Composite.Anchor.TOP_LEFT);
		composites.clear();
		composites.add(backgroundComposite);

		composites
				.addAll(createComposites(
						Profile.NUMBER_FORMAT.format(profile.getKills()), 282,
						10, true));
		composites.addAll(createComposites(
				Profile.NUMBER_FORMAT.format(profile.getDeaths()), 387, 10,
				true));
		composites.addAll(createComposites(
				Profile.NUMBER_FORMAT.format(profile.getDamages()), 503, 10,
				true));

		background = imagesService.composite(composites, background.getWidth(),
				background.getHeight(), 0);
		backgroundComposite = ImagesServiceFactory.makeComposite(background, 0,
				0, 1.0f, Composite.Anchor.TOP_LEFT);

		composites.clear();
		composites.add(backgroundComposite);

		composites.addAll(createComposites(
				Profile.NUMBER_FORMAT.format(profile.getSorties()), 282, 33,
				true));
		composites.addAll(createComposites(
				Profile.FLOAT_FORMAT.format(profile.getKillDeathRatio()), 387,
				33, true));

		if (profile.getBranch() == Branch.AirForce) {
			composites.addAll(createComposites(
					Profile.FLOAT_FORMAT.format(profile.getKillDamageRatio()),
					503, 33, true));
		} else {
			composites.addAll(createComposites(
					Profile.NUMBER_FORMAT.format(profile.getCaptures()), 503,
					33, true));
		}

		Image image = imagesService.composite(composites,
				background.getWidth(), background.getHeight(), 0);

		if (log.isLoggable(Level.FINER)) {
			log.finer("Image width : " + image.getWidth());
			log.finer("Image height : " + image.getHeight());
		}

		return image;
	}

	protected Collection<Composite> createComposites(String text, int offsetX,
			int offsetY, boolean numeric) {
		Collection<Composite> composites = new ArrayList<Composite>();

		int offset = 0;
		int bias = numeric ? text.length() * -LETTER_WIDTH : 0;

		for (char c : text.toCharArray()) {
			Image image = letters.get(c);

			if (image == null) {
				continue;
			}

			Composite composite = ImagesServiceFactory.makeComposite(image,
					offset + offsetX + bias, offsetY, 1.0f,
					Composite.Anchor.TOP_LEFT);
			composites.add(composite);

			offset += LETTER_WIDTH;
		}

		return composites;
	}
}
